/*
 * Licensed to Nanjing RedOrange Co. ltd (RedOrange) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * RedOrange licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package redora.rdo.generator;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathExpressionException;

import org.apache.commons.io.IOUtils;
import org.dom4j.Element;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import redora.rdo.generator.Template.Destination;
import redora.rdo.generator.Template.Input;
import redora.rdo.generator.exceptions.ModelGenerationException;
import redora.rdo.util.DefaultErrorHandler;
import redora.rdo.util.SchemaValidator;
import freemarker.template.TemplateException;

/**
 * Retrieves all available models from the file system in this project and
 * generated all the sources and resources with the available templates. The
 * maven plugin will use the ModelProcessor directly, so just add the maven
 * plugin to your project's plugins.
 * 
 * @author jaap
 */
public class ModelProcessor {

	public static final String SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";

	public static final String XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";

	public static final String SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";

	/** Schema file for checking if the model document is correct. */
	public static final String MODEL_SCHEMA = "/model.xsd";

	/** Schema file for checking if the include document is correct. */
	public static final String INCLUDE_SCHEMA = "/include.xsd";

	/**
	 * Generate sources for project
	*/
	public static void generate(String basePackage, String defaultLanguage,
			String sourceDir, String buildDir, String resourceDir, String webappDir, String artifactId)
			throws ParserConfigurationException, TemplateException,
			XPathExpressionException, TransformerException,
			ModelGenerationException, SAXException, IOException {
		TemplateHandler t = new TemplateHandler();
		ModelFileHandler p = new ModelFileHandler(sourceDir, buildDir,
				resourceDir, webappDir);
		Document allModels = newDocument(basePackage, defaultLanguage,
				"objects");
		// Check the model files if all is ready to go
		for (String modelFile : p.findModelFiles()) {
			try {
				if (!ModelProcessor.validate(MODEL_SCHEMA, p.modelDir()
						+ modelFile)) {
					throw new ModelGenerationException("Model file "
							+ modelFile
							+ " failed the schema validation, i will stop now."
							+ ModelGenerationException.printModel(p, modelFile));
				}
			} catch (SAXException e) {
				throw new ModelGenerationException("Model file " + modelFile
						+ " failed. "
						+ ModelGenerationException.printModel(p, modelFile), e);
			} catch (IOException e) {
				throw new ModelGenerationException("Model file " + modelFile
						+ " failed. "
						+ ModelGenerationException.printModel(p, modelFile), e);
			}
		}
		for (String includeFile : p.findIncludeFiles()) {
			try {
				if (!ModelProcessor.validate(INCLUDE_SCHEMA, p.includeDir()
						+ includeFile)) {
					throw new ModelGenerationException("Include file "
							+ includeFile
							+ " failed the schema validation, i will stop now."
							+ ModelGenerationException.printInclude(p,
									includeFile));
				}
			} catch (SAXException e) {
				throw new ModelGenerationException(
						"Include file "
								+ includeFile
								+ " failed. "
								+ ModelGenerationException.printInclude(p,
										includeFile), e);
			} catch (IOException e) {
				throw new ModelGenerationException(
						"Could not open this include: " + includeFile, e);
			}
		}

		// Run the per model generation stuff
		for (String modelFile : p.findModelFiles()) {
			String modelName = modelFile.replace(".xml", "");

			Document model = model(p.modelDir(), modelName, basePackage);
			allModels.getFirstChild().appendChild(
					allModels.importNode(model.getFirstChild(), true));

			for (Template tpl : t.byInput(Input.Model)) {
				if (tpl.ignoreProjects == null || !tpl.ignoreProjects.contains(artifactId)) {
					p.process(tpl, model, basePackage + "." + tpl.packageSuffix, tpl.getDestinationFileName(modelName, null, null), null);
				} else {
					System.out.println("Ignoring " + tpl.name + " for " + artifactId);
				}
			}
		}
		for (Template tpl : t.byInput(Input.AllModels)) {
			if (tpl.ignoreProjects == null || !tpl.ignoreProjects.contains(artifactId)) {
				p.process(tpl, allModels, basePackage + "." + tpl.packageSuffix, tpl.getDestinationFileName(null, null, null), null);
			} else {
				System.out.println("Ignoring " + tpl.name + " for " + artifactId);
			}
		}
		
		Map<String, Document> enumerations = XMLUtil.enumerations(allModels,
				basePackage);
		for (String key : enumerations.keySet()) {
			System.out.println("Processing enum " + key);
			for (Template tpl : t.byInput(Input.Enum)) {
				if (tpl.ignoreProjects == null || !tpl.ignoreProjects.contains(artifactId)) {
					p.process(tpl, enumerations.get(key), basePackage + "." + tpl.packageSuffix, tpl.getDestinationFileName(null, null, key), null);
				} else {
					System.out.println("Ignoring " + tpl.name + " for " + artifactId);
				}
			}
		}
		
		for (String language : XMLUtil.definedLanguages(allModels)) {
			HashMap<String, String> langParam = new HashMap<String, String>();
			langParam.put("language", language);
			System.out.println("Processing language " + language);
			for (Template tpl : t.byInput(Input.Language)) {
				if (tpl.ignoreProjects == null || !tpl.ignoreProjects.contains(artifactId)) {
					p.process(tpl, allModels, basePackage + "." + tpl.packageSuffix, tpl.getDestinationFileName(null, language, null), langParam);
				} else {
					System.out.println("Ignoring " + tpl.name + " for " + artifactId);
				}
			}
		}
	}

	/**
	 * Create the model XML document and add the <name/> tag so the name can
	 * also can be parsed by FreeMarker.
	 * 
	 * @param modelName
	 *            The file name is used as model name and put to the 'name'
	 *            element in the document
	 * @param modelDir
	 *            The model directory
	 * @param packageName
	 *            (Optional) Adds a 'package' element, it will the the root's
	 *            first child
	 * @return Document The Model document
	 */
	public static Document model(String modelDir, String modelName,
			String packageName) throws ModelGenerationException {
		Document doc = null;
		String modelFileName = modelDir + modelName + ".xml";
		try {
			doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
					.parse(new File(modelFileName));
			loadIncludes(doc, modelDir);
		} catch (SAXException e) {
			throw new ModelGenerationException(
					"Strange, i failed to create a DOM of this model "
							+ modelName + " (" + modelFileName + ")", e);
		} catch (IOException e) {
			throw new ModelGenerationException("I failed to find this file: "
					+ modelFileName, e);
		} catch (ParserConfigurationException e) {
			throw new ModelGenerationException(
					"Strange, the DOM is not configured right, it worked before! Maybe you have some system properties set that interfere with me"
							+ " (" + modelName + ")", e);
		}
		// The file name is also the object name!
		addChildElement(doc, "name", modelName);
		addChildElement(doc, "package", packageName);

		return doc;
	}

	/**
	 * Create the model XML document and adds the root element and optional a
	 * package element. This method is simple and created for your convenience.
	 * 
	 * @param basePackage
	 *            (Optional) Adds a 'package' element, it will the the root's
	 *            first child
	 * @param defaultLanguage
	 *            (Optional) Adds a 'defaultLanguage' element
	 * @param root
	 *            (Mandatory) root element name
	 * @return Document The Model document
	 */
	public static Document newDocument(String basePackage,
			String defaultLanguage, String root)
			throws ModelGenerationException {
		Document doc = null;

		try {
			doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
					.newDocument();
		} catch (ParserConfigurationException e) {
			throw new ModelGenerationException(
					"Strange, the DOM is not configured right, it worked before! Maybe you have some system properties set that interfere with me",
					e);
		}

		Node insertNode = doc.createElement(root);
		doc.appendChild(doc.importNode(insertNode, true));
		if (basePackage != null)
			addChildElement(doc, "package", basePackage);
		if (defaultLanguage != null)
			addChildElement(doc, "defaultLanguage", defaultLanguage);
		return doc;
	}

	/**
	 * Adds an child element to the document's root element.
	 * 
	 * @param doc
	 *            The XML document
	 * @param tag
	 *            The element's name
	 * @param value
	 *            It's value
	 */
	public static void addChildElement(Document doc, String tag, String value) {
		if (doc.getElementsByTagName(tag).getLength() > 0) {
			doc.getElementsByTagName(tag).item(0).getChildNodes().item(0)
					.setNodeValue(value);
		} else {
			Node tagNode = doc.createElement(tag);
			tagNode.setTextContent(value);
			doc.getFirstChild().appendChild(tagNode);
		}

	}

	/**
	 * Validate the xml file according to schema file MODEL_SCHEMA
	 * 
	 * @param xmlFileName
	 *            xmlFileName
	 * @return True if the XML file validates against the XSD.
	 * @throws ModelGenerationException
	 */
	@SuppressWarnings("unchecked")
	public static boolean validate(String schemaFile, String xmlFileName)
			throws ModelGenerationException, SAXException, IOException {
		File xmlFile = new File(xmlFileName);
		DefaultErrorHandler errorHandler = new DefaultErrorHandler();

		if (!xmlFile.exists()) {
			throw new ModelGenerationException("xml file:" + xmlFileName
					+ " does not exist");
		}
		SchemaValidator validator;
		if (schemaFile.equalsIgnoreCase(INCLUDE_SCHEMA)) {
			String include = IOUtils.toString(ModelProcessor.class
					.getResourceAsStream(INCLUDE_SCHEMA));
			String model = IOUtils.toString(ModelProcessor.class
					.getResourceAsStream(MODEL_SCHEMA));
			InputStream includeStream = IOUtils
					.toInputStream(include
							.replace(
									"<xs:include schemaLocation=\"model.xsd\" />",
									model
											.substring(
													model
															.indexOf("<xs:complexType name=\"attributesType\">"),
													model
															.lastIndexOf("</xs:schema>"))));
			validator = new SchemaValidator(includeStream);
		} else {
			validator = new SchemaValidator(ModelProcessor.class
					.getResourceAsStream(MODEL_SCHEMA));
		}

		validator.validate(xmlFile, errorHandler);

		if (errorHandler.getErrors().hasContent()) {
			System.out.println("XML file:" + xmlFileName
					+ " failed to do checking accroding to XSD file:");
			Iterator<Element> ei = errorHandler.getErrors().elementIterator();
			while (ei.hasNext()) {
				Element el = ei.next();
				System.out.println(el.getStringValue());
			}

			return false;
		}

		System.out.println("XML file:" + xmlFileName
				+ " succeed to do checking accroding to XSD file!");
		return true;
	}

	/**
	 * Load includes
	 * 
	 * @param doc
	 *            The XML document
	 * @param modelDir
	 * @throws ModelGenerationException
	 * */

	public static void loadIncludes(Document doc, String modelDir)
			throws ModelGenerationException {
		String modelFileName = null;
		NodeList includes = doc.getElementsByTagName("include");
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

		for (int i = 0; i < includes.getLength(); i++) {
			modelFileName = modelDir
					+ "includes"
					+ File.separator
					+ includes.item(i).getAttributes().getNamedItem("name")
							.getNodeValue() + ".xml";

			Document includeDoc = null;
			try {
				includeDoc = factory.newDocumentBuilder().parse(
						new File(modelFileName));
			} catch (SAXException e) {
				throw new ModelGenerationException(
						"I failed to create a DOM of this include "
								+ modelFileName, e);
			} catch (IOException e) {
				throw new ModelGenerationException(
						"I failed to find this include file: " + modelFileName,
						e);
			} catch (ParserConfigurationException e) {
				throw new ModelGenerationException(
						"I failed to create a DOM of this include "
								+ modelFileName, e);
			}
			Node parent = includes.item(i).getParentNode();
			for (int j = 0; j < includeDoc.getDocumentElement().getChildNodes()
					.getLength(); j++) {
				Node insert = includeDoc.getFirstChild().getChildNodes()
						.item(j);
				if (insert.getNodeType() == Node.ELEMENT_NODE) {
					// TODO this doesnt work, but it must be set up sort of in
					// this way because now we append the include at the bottom
					// of attributes
					// doc.insertBefore(doc.importNode(insert,
					// true).cloneNode(true), includes.item(i));
					parent.appendChild(doc.importNode(insert, true));
				}
			}
			parent.removeChild(includes.item(i));
		}
		doc.normalize();
	}
}
